package com.openess.bigsearch.engine.search;

import java.io.IOException;

import com.openess.bigsearch.engine.index.Extent;
import com.openess.bigsearch.engine.index.Token;
import com.openess.bigsearch.engine.search.IndexSearcher.DocScore;
import com.openess.bigsearch.engine.utils.HashMap;
import com.openess.bigsearch.engine.utils.LinkedList;
import com.openess.bigsearch.engine.utils.PriorityQueue;

/**
 * 自动摘要，目前版本只能对content域进行摘要，即文章原文进行摘要。如果想摘要别的域，现在无法做到。还有可优化的空间，应该把最优摘要计算出来以后，
 * 再进行窗口的优化操作，而不是先把所有窗口优化一遍再计算分数
 * 
 * @author TuTu
 * 
 */
public class Summary {
	/**
	 * 滑动窗口的最短长度，单位：字符
	 */
	public static final int FIXED = 100;
	/**
	 * 滑动窗口的前后延长长度
	 */
	public static final int UNFIXED = 25;
	/**
	 * 滑动窗口在优化的时候，所依赖的优化点
	 */
	public static final String[] OPTMIZE = new String[] { "\n", "。", ".", "，",
			"，", "！", "!", "；", ";", ":", "：" };
	/**
	 * 往set集合中传入id: (weight, pos[])，整合成id1: (weight1, pos1[]) ( weight2,
	 * pos2[])..., id2: ()..., id3: ()...
	 */
	private HashMap<Long, LinkedList<Data>> map;

	public Summary(Extent ext) {
		map = new HashMap<Long, LinkedList<Data>>();
	}

	/**
	 * 往map集合中传入id: (weight, pos[])，整合成id1: (weight1, pos1[]) (weight2, pos2[])
	 * id2... id3...。本方法隐含使用了term-at-a-time算法，因为传入的id: (weight, pos[]
	 * )并不是一篇文档中的所有pos信息，而是只属于一个查询词项的，通过map集合整合就变成一篇文档中的所有pos了
	 * 
	 * @param id
	 *            分词所在文档ID
	 * @param weight
	 *            分词在文档中的权重值(TF*IDF)
	 * @param pos
	 *            分词在id指定的文档中的所有位置
	 */
	public void add(long id, double weight, Integer[] pos) {
		Data d = new Data(weight, pos);
		LinkedList<Data> value = map.get(id);
		if (value == null) {
			value = new LinkedList<Data>();
			value.add(d);
			map.put(id, value);
		} else
			value.add(d);
	}

	/**
	 * 将set中的id: (pos1, weight1) (pos2, weight2)取出来，利用id拿到原文本，并计算摘要，得到指定文档最合适的摘要
	 * 
	 * @param ds
	 *            ds代表一个文档，仅计算指定ID文档的摘要 (排序后保存的有限个数的文档) ，而不是之前版本的查询词返回的所有的文档
	 * @throws IOException
	 * @return 返回指定文档的最优摘要
	 */
	public String compute(DocScore ds) throws IOException {
		long docID = Long.valueOf(ds.getDocID());
		// 有可能text不含查询分词
		String text = ds.getContent();
		// content域始终放在最开始的位置
		int startOffset = 0;
		// set中包括所有查询词的文档ID，但我们只需要其中的排名最靠前的几个
		LinkedList<Data> list = map.get(docID);
		// 计算摘要，并将每篇文档最适合的结果(id, summary)放入链表中
		return process(docID, text, startOffset, list);
	}

	/**
	 * 根据传入的参数，计算一篇文档的最合适摘要，内部对多个备选摘要进行打分选择出最好的
	 * 
	 * @param id
	 *            抽取摘要的文档ID
	 * @param text
	 *            文档原文
	 * @param startOffset
	 *            域的起始偏移量，这个信息很重要。因为我们拿到的是单独一个域的文本，但分词偏移量是基于所有域位置的，
	 *            因此想转换成单独域中的偏移量，就必须减去域的起始位置。
	 * @param list
	 *            计算摘要所需的数据，(pos, weight)都放在链表中了
	 * @return 返回最优摘要
	 */
	private String process(long id, String text, int startOffset,
			LinkedList<Data> list) {
		Sumy sumy = new Sumy(list);
		// 每循环一次，得到一个查询分词在指定文档中的所有备选摘要，第二次循环计算下一个查询词的备选摘要
		for (Data d : list) {
			// 一个分词在一篇文档中的所有位置
			Integer[] posList = d.posList;
			// 每循环一次，得到同一篇文档中的一个备选摘要
			for (int pos : posList) {
				pos -= startOffset;
				// pos > text.length说明content中不含查询词,没法做摘要,直接截取content前100个字符
				if (pos > text.length()) {
					sumy.add(text, 0, FIXED);
					continue;
				}
				int start = pos - UNFIXED;
				int end = pos + FIXED;
				// 如果从指定分词位置开始的固定窗口小于指定的尺寸，那么end为文本末尾
				if (end > text.length())
					end = text.length();
				// 如果start小于0那么，原文本前面补空格来填充
				if (start < 0) {
					for (; start == 0; start++) {
						text = " " + text;
						end++;
					}
					start = 0;
				}
				// 未优化前的固定窗口
				String raw = text.substring(start, end);
				// 将备选窗口放入Extent中，准备计算最高得分，目前的分数计算，只计算固定窗口，而不管优化窗口
				sumy.add(raw, start, end);
			}
		}
		// 拿到Extent计算的得分最高的摘要
		String raw = sumy.getTopSumy(startOffset);
		// 优化得分最高的摘要
		return optimize(raw);
	}

	/**
	 * 对得分最高的摘要进行优化，使得返回的摘要更完整，增加摘要的可读性。在raw固定摘要前后摘取指定的字符，对raw进行磨边，如果没有可优化的地方，
	 * 那么返回raw固定摘要。
	 * 
	 * @param raw
	 *            每篇文档中得分最高的摘要
	 */
	private String optimize(String raw) {
		if (raw.length() < 2 * UNFIXED)
			return raw;
		int beIndex = -1;
		int afIndex = -1;
		String before = raw.substring(0, UNFIXED);
		String after = raw.substring(raw.length() - UNFIXED);
		for (String str : OPTMIZE) {
			// 如果上一个str没有在字符串中找到，那么继续找optimize数组中的下一个，直到找到为止
			if (beIndex == -1) {
				// -1说明没找到
				beIndex = before.indexOf(str);
			}
			if (afIndex == -1) {
				afIndex = after.indexOf(str);
			}
			if (beIndex != -1 && afIndex != -1) {
				break;
			}
		}
		/*
		 * for(int i = 0; i <UNFIXED;i++) { String str1 = before.charAt(i) + "";
		 * String str2 = after.charAt(i) + ""; if(opSet.contain(str1) && beIndex
		 * == -1) { beIndex = i; } if(opSet.contain(str2) && afIndex == -1) {
		 * afIndex = i; } if (beIndex != -1 && afIndex != -1) { break; } }
		 */
		// 对备选滑动窗口进行优化，优化使得窗口可读性更高。
		// 如果窗口右侧没有找到可优化的断句符号，那么暂时以离固定窗口最近的空格作为分割点，因为要保证不切分完整的英文单词。
		if (afIndex == -1) {
			afIndex = after.indexOf(" ");
		}
		if (afIndex != -1) {
			// 加1是为了把过滤的标点符号，包括进摘要中
			raw = raw.substring(0, afIndex + raw.length() - UNFIXED + 1);
		}
		if (beIndex == -1) {
			beIndex = before.indexOf(" ");
		}
		// 顺序不能颠倒，必须先截取后短，才是前段
		if (beIndex != -1) {
			// !1是为去掉前部分的过滤标点符号
			raw = raw.substring(beIndex + 1);
		}
		return raw;
	}

	/**
	 * 这个类用来计算一篇文档中每个备选窗口的得分，并返回最合适的摘要，每篇文档的摘要计算都需要单独创建一个本类对象。类中保存主要有两种数据：1.
	 * 优化后的窗口文本，2.窗口在原文本中的起始和终止位置，[start, end)
	 * 
	 * @author TuTu
	 * 
	 */
	private class Sumy {
		/**
		 * 包括有(raw, (start, end))
		 */
		private LinkedList<Token<String, Token<Integer, Integer>>> list1;
		/**
		 * (weight, pos[])链表，链表中的每一个元素代表一个分词在一篇文档中的所有信息，用来计算每个备选窗口的得分，
		 * list2中的所有数据都属于一篇文档
		 */
		private LinkedList<Data> list2;
		/**
		 * 将getTopSumy方法中计算得到的最优结果保存在队列中,用优先队列比较省空间
		 */
		private PriorityQueue<Token<Double, String>> que;

		/**
		 * 构造方法， 传入(weight, pos[])链表，链表中的每一个元素代表一个分词在一篇文档中的所有信息，用来计算每个备选窗口的得分
		 * 
		 * @param list2
		 */
		public Sumy(LinkedList<Data> list2) {
			this.list1 = new LinkedList<Token<String, Token<Integer, Integer>>>();
			this.list2 = list2;
			this.que = new PriorityQueue<Token<Double, String>>(1);
		}

		/**
		 * 往集合中添加一个备选摘要片段，以及这个片段在原文中的范围
		 * 
		 * @param raw
		 *            优化后的片段
		 * @param start
		 *            片段在原文中的起始位置
		 * @param end
		 *            片段在原文中的终止位置，不包括
		 */
		private void add(String raw, int start, int end) {
			Token<Integer, Integer> ext = new Token<Integer, Integer>(start,
					end);
			Token<String, Token<Integer, Integer>> t = new Token<String, Token<Integer, Integer>>(
					raw, ext);
			list1.add(t);
		}

		/**
		 * 计算一篇文档的最高分摘要,将摘要保存在队列中。一边计算摘要一边推入队列，循环结束即可得到最高分摘要，比拿到所有结果再计算最高分，
		 * 要效率高,节省空间
		 * 
		 * @param startOffset
		 *            域的起始偏移位置,因为pos是全局的位置信息，必须减去startOffset才能得到局部位置
		 * 
		 * @return 返回最优摘要
		 */
		private String getTopSumy(int startOffset) {
			// 每循环一次，计算一个备选摘要的分数
			for (Token<String, Token<Integer, Integer>> t1 : list1) {
				// 每个备选摘要的分数
				Double score = 0d;
				String raw = t1.getKey();
				int start = t1.getValue().getKey();
				int end = t1.getValue().getValue();
				// 每循环一次，判断一个分词的所有位置
				for (Data d : list2) {
					double weight = d.weight;
					Integer[] posList = d.posList;
					score += contain(startOffset, posList, start, end) * weight;
				}
				que.enqueue(new Token<Double, String>(score, raw));
			}
			// 返回最优摘要
			if (que.isEmpty()) {
				return null;
			}
			return que.dequeue().getValue();
		}

		/**
		 * 判断指定的pos是不是在范围内
		 * 
		 * @param startOffset
		 *            域的起始偏移位置,因为pos是全局的位置信息，必须减去startOffset才能得到局部位置
		 * @param pos
		 * @param start
		 * @param end
		 * @return 共有几个pos符合要求，0代表都不符合
		 */
		private int contain(int startOffset, Integer[] posList, int start,
				int end) {
			int count = 0;
			for (int pos : posList) {
				pos -= startOffset;
				if (pos >= start && pos < end) {
					count++;
				}
			}
			return count;
		}
	}

	/**
	 * (weight, pos[])
	 * 
	 * @author xiaoshuo
	 * 
	 */
	private class Data {
		private Double weight;
		private Integer[] posList;

		public Data(Double weight, Integer[] posList) {
			this.weight = weight;
			this.posList = posList;
		}
	}
}
